示範中使用卡片佈局 (card layout) 顯示 Github 使用者。GithubProfileList 會迭代一個使用者名稱的清單,並在 GithubProfileCard 中顯示每位使用者。
// components/GithubProfileCard.vue
<script setup lang="ts">
import { useGithubProfile } from '@/composables/useGithubProfile.ts'
type Prop = {
    username: string
}
const { username } = defineProps<Prop>()
</script>
GithubProfileCard 預期接收一個 username 的 prop,該 prop 可指派給 useGithubProfile composable 中的 username ref。該專案是使用 TypeScript 撰寫,因此 defineProps 巨集(macro)可以接受一個 prop 類型。
<script setup lang="ts">
const { username: name, profile, error } = useGithubProfile()
name.value = username
</script>
從 useGithubProfile composable 中解構(destructure)username、profile 和 error refs。username 被別名為 name,以避免與 username prop 發生衝突。
當 name 這個 ref 更新時,composable 會取得 Github 個人資料,並將結果存放在 profile ref 中。
<template>
    <div v-if="profile">
        <p>Username: {{ profile.login }}</p>
        <p>Name: {{ profile.name }}</p>
        <p>Bio: {{ profile.bio || 'N/A' }}</p>
    </div>
    <div v-else-if="error">
        Error: {{ error }}
    </div>
</template>
v-if 指令會檢查 profile 這個 ref 的值。當 profile 被定義時,模板會顯示登入帳號(login)、名稱(name)和個人簡介(bio)。
而 v-else-if 指令則會檢查 error 的值,當 error 不為空白時,會顯示錯誤訊息。
// github-profile.type.ts
export type GithubProfileItem = { 
    key: number; 
    profile?: GithubProfile; 
    error?: string 
}
GithubProfileItem 類型由 profile 或 error 屬性組成。
// github-profile-card.svelte
<script lang="ts">
    import type { GithubProfileItem } from './github-profile-item.type';
    type Props = {
        profile: GithubProfileItem
    };
    const { profile: result }: Props = $props();
    const { profile, error } = result;
</script>
在這個元件中,$props() 被轉型(cast)為 Props 類型。接著從 prop 中解構出 profile 和 error。
{#if profile}
    <div>
        <p>Username: {profile.login}</p>
        <p>Name: {profile.name}</p>
        <p>Bio: {profile.bio || 'N/A'}</p>
    </div>
{:else if error}
    <div>
        <p>Error: {error}</p>
    </div>
{/if}
在模板中,if-else-if 控制流程語法會檢查 profile 和 error 兩者的值。當 profile 被定義時,if 分支會顯示 Github 個人資料;而當 error 不為空白時,else-if 分支會顯示錯誤訊息。
// github-profile-card.component.ts
import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { GithubProfile } from '../types/github-profile.type';
@Component({
    selector: 'app-github-profile-card',
    templateUrl: './github-profile-card.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GithubProfileCardComponent {
    username = input.required<string>();
    profileResource = httpResource<GithubProfile>(() => this.username() ? { 
        url: `https://api.github.com/users/${this.username()}`,
        method: 'GET',
        headers: {
            Authorization: `Bearer ${GITHUB_TOKEN}`
        }
    }: undefined, {
        equal: (a, b) => a?.login === b?.login,
    });
    profile = computed(() => this.profileResource.hasValue() ? this.profileResource.value() : undefined);
    error = computed(() => this.profileResource.error()?.message || '');
}
元件有一個必須的信號輸入 (required signal input),名為 username。
當 username 輸入 (input) 改變時,profileResource 會以反應式方式建立一個 HttpResourceRequest。
{ 
    url: `https://api.github.com/users/${this.username()}`,
    method: 'GET',
    headers: {
        Authorization: `Bearer ${GITHUB_TOKEN}`
    }
}
該請求包含 Github 的 URL、HTTP 方法和 HTTP 標頭 (header)。
當 profileResource 有新值時,profile 和 error 這兩個計算信號(computed signals)分別衍生出對應的個人資料和錯誤訊息。
// github-profile-card.component.html
@let status = profileResource.status();
@if (status === 'loading') {
    <p>Loading profile...</p>
} @else if (status === 'error') {
    <p>Error loading profile: {{ error() }}</p>
} @else {
    @if (profile(); as profile) {
        <div>
            <p>Username: {{ profile.login }}</p>
            <p>Name: {{ profile.name }}</p>
            <p>Bio: {{ profile.bio || 'N/A' }}</p>
        </div>
    }
}
HTML 模板會根據狀態 (state) 條件性顯示內容:
接下來,我們建立一個 GithubProfileList 元件,該元件會將使用者名稱傳遞給 GithubProfileCard 以進行呈現。GithubProfileList 是可重複使用的,因為它也能接受任何使用者名稱的清單。
// GithubProfileList.vue
<script setup lang="ts">
    import GithubProfileCard from './GithubProfileCard.vue'
    const { usernames } = defineProps<{ usernames: string[] }>()
</script>
從 defineProps 中解構出 usernames 清單。
<template>
  <div class="header">
    <h1>Github Profile List (Vue 3 Ver.)</h1>
  </div>
  <GithubProfileCard v-for="username in usernames" :key="username" :username="username" />
</template>
v-for 指令 (directive) 迭代 username prop,並將 username 同時綁定為 key 和 username 屬性。
// App.vue
<script setup lang="ts">
import GithubProfileList from './components/GithubProfileList.vue'
const usernames = ['johnsoncodehk', 'antfu', 'railsstudent', 'danielkellyio', 'hootlex', 'MooseSaeed']
</script>
<template>
  <GithubProfileList :usernames="usernames" />
</template>
App 元件會將 usernames 清單傳遞給 GithubProfileList。
// github-profile-list.svelte
<script lang="ts"> 
    import GithubProfileCard from "./github-profile-card.svelte";
	import type { GithubProfileItem } from './github-profile-item.type';
    type Props = {
        profiles: GithubProfileItem[]
    };
    const { profiles }: Props = $props();
</script>
GithubProfileList 從 $props() 解構(destructure)屬性(props)。
<div class="header">
    <h1>Github Profile List (Svelte ver.)</h1>
</div>
{#each profiles as profile (profile.key)}
    <GithubProfileCard {profile} />
{/each}
在模板中,#each 會迭代 profiles,並將 profile 綁定到 GithubProfileCard 的屬性(props)上。
// +page.svelte
<script lang="ts">
	import type { PageProps } from './$types';
	import GithubProfileList from '$lib/github-profile-list.svelte';
	const { data: results }: PageProps = $props();
	const { data: profiles } = results;
</script>
{#if !profiles || profiles.length === 0}
	<p>No profiles found.</p>
{:else}
	<GithubProfileList {profiles} />
{/if}
loader 函式會載入 profiles,並將其作為屬性傳遞給 GithubProfileList。
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { GithubProfileCardComponent } from './github-profile-card.coponent';
@Component({
    selector: 'app-github-profile-list',
    imports: [GithubProfileCardComponent],
    template: `
        <div class="header">
            <h1>Github Profile List (Angular Ver.)</h1>
        </div>
        @for (username of usernames(); track username) {
            <app-github-profile-card [username]="username"/>
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GithubProfileListComponent {
    usernames = input.required<string[]>();   
}
元件有一個必需的信號輸入 (signal input),為 usernames。
@for 會迭代 usernames 輸入 (input) 並將其中的 username 傳遞給 GithubProfileCardComponent 的 username 信號輸入 (signal input)。
// app.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { GithubProfileListComponent } from './github/components/github-profile-list.component';
@Component({
  selector: 'app-root',
  imports: [GithubProfileListComponent],
  template: '<app-github-profile-list [usernames]="usernames" />',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  readonly usernames = ['johnsoncodehk', 'antfu', 'railsstudent', 'danielkellyio', 'hootlex', 'MooseSaeed'];   
}
AppComponent 定義了一個唯讀的 usernames 清單,並將其傳遞給 GithubProfileListComponent。
我們已成功建立了 GithubProfileList 和 GithubProfileCard 元件,並將名稱從父元件傳遞到子元件。子元件接收輸入並在模板中顯示它們。